import { AnyHolder, Reject, Resolve } from "@/utils/AnyHolder";
import { Socket, connect } from "socket.io-client";
import { CodeEnum } from "./CodeEnum";
import { IC2SEventCallbackMap } from "./IC2SEventCallbackMap";
import { IKeyFrame } from "./IKeyFrame";
import { IMember } from "./IMember";
import { IMemberInfo } from "./IMemberInfo";
import { IResult } from "./IResult";
import { IS2CEventCallbackMap } from "./IS2CEventCallbackMap";
import { Member } from "./Member";
import { RTCEventMap } from "./RTCEventMap";
import { INFO, INFO_ARGS, WARN } from "./utils";
interface IJoinRoomData { members: IMemberInfo[] }
interface IJoinRoomResult extends IResult<IJoinRoomData> { }

export type RTCSocket = Socket<IS2CEventCallbackMap, IC2SEventCallbackMap>;
export interface RTCConfig {
  host?: string;
}
export enum RoomStatus {
  Idle = 'idle',
  Joining = 'joining',
  Joined = 'joined',
}

function _reject(msg: string) {
  return Promise.reject(new Error(msg))
}
export class RTC<KeyFrameDataMap extends unknown = any, KeyFrame extends IKeyFrame<KeyFrameDataMap> = IKeyFrame<KeyFrameDataMap>> {
  readonly config: RTCConfig;
  constructor(config: RTCConfig = {}) {
    (window as any).rtc = this;
    this.config = config;
  }
  protected _et = document.createElement('div');
  protected _room_id: string = '';
  protected _session_id: string = '';
  protected _user_id: string = '';
  protected _room_status: RoomStatus = RoomStatus.Idle
  protected _members = new Map<string, IMember>()

  get room_id() { return this._room_id }
  get session_id() { return this._session_id }
  get user_id() { return this._user_id }
  get room_status() { return this._room_status }
  get members(): ReadonlyMap<string, IMember> { return this._members }

  @INFO
  join_room(user_id: string, session_id: string, room_id: string) {
    this._user_id = user_id
    this._session_id = session_id;
    this._room_id = room_id;
    switch (this._room_status) {
      case RoomStatus.Joining: return _reject("already joining!");
      case RoomStatus.Joined: return _reject("already joined!");
    }
    this._room_status = RoomStatus.Joining;
    return new Promise<IJoinRoomResult>((resolve, reject) => {
      this.socket.then(socket => {
        socket.timeout(5000).emit(
          "join_room",
          session_id,
          room_id,
          (error, result) => {
            try {
              this._on_join_room(error, result)
              this._room_status = RoomStatus.Joined
              resolve(result)
            } catch (e) {
              reject(e)
              this._room_status = RoomStatus.Idle
            }
          }
        )
      }).catch(e => {
        reject(e);
        this._room_status = RoomStatus.Idle
      })
    })
  }

  @INFO
  exit_room(room_id: string) {
    this._room_status = RoomStatus.Idle
    const socket = this._socket.direct;
    socket?.emit("exit_room", room_id, () => this._on_exit_room())
    socket?.disconnect()
  }

  @INFO
  async send_key_frames(key_frames: KeyFrame[]) {
    const socket = await this.socket;
    socket.emit("key_frames", key_frames, () => { })
  }

  @INFO
  on_track(ev: RTCTrackEvent) { }

  get socket() { return this._socket.value }
  protected _socket = new AnyHolder<RTCSocket>((resolve, reject, clear) => {
    try {
      if (!this.config.host) throw new Error('host not set!')
      INFO.print("connecting to", this.config.host)
      const inst: RTCSocket = connect(this.config.host, { transports: ["websocket"] });
      inst.on("negotiation", this._on_negotiation.bind(this))
      inst.on("othter_join_room", r => this._on_someone_join_room(r))
      inst.on("othter_exit_room", r => this._on_someone_exit_room(r))
      inst.on("key_frames", r => this._on_key_frames_recv(r as any))
      inst.once("connect", () => this._on_socket_connect(inst));
      inst.once("will_disconnect", reason => this._on_socket_will_disconnect(reason));
      inst.once("disconnect", r => this._on_socket_disconnect(r));
      inst.once("disconnect", () => clear());
      inst.once("disconnect", () => inst.removeAllListeners());
      resolve(inst)
    } catch (e) {
      reject(e);
    }
  });


  @INFO_ARGS
  protected _on_negotiation(desc: RTCSessionDescriptionInit) {
  }

  @INFO
  protected _on_socket_connect(inst: RTCSocket) { }

  @INFO_ARGS
  protected _on_socket_will_disconnect(reason: string) {
  }

  @INFO_ARGS
  protected _on_socket_disconnect(inst: Socket.DisconnectReason) {
    this._on_exit_room()
  }

  @INFO_ARGS
  protected _on_join_room(error: Error | null, r: IJoinRoomResult) {
    if (error) throw error
    if (r.code !== CodeEnum.Ok) throw new Error(`[_on_join_room] failed! [${CodeEnum[r.code]}]${r.msg}`)
    if (!r.data) throw new Error(`[_on_join_room] data got ${r.data}`)
    for (const member of r.data.members) {
      this._on_someone_join_room(member);
    }
  }

  @INFO
  protected _on_exit_room() {
    this._members.forEach(({ info }) => this._on_someone_exit_room(info))
    this._members.clear();
  }


  @INFO_ARGS
  protected _on_someone_join_room(r: IMemberInfo): void {
    let room_member = this._members.get(r.user_id);
    if (room_member) {
      room_member.info = r
      this.dispatch("someone_join_room", room_member)
      return;
    }
    room_member = (r.user_id === this._user_id) ?
      new Member(r, this.preview_stream) :
      new Member(r, this.preview_stream, this._socket).set_polite(r.user_id < this._user_id)
    this._members.set(r.user_id, room_member)
    this.dispatch("someone_join_room", room_member)
  }

  @INFO_ARGS
  protected _on_someone_exit_room(r: IMemberInfo): void {
    const room_member = this._members.get(r.user_id);
    if (!room_member) return;
    room_member.destructor();
    this._members.delete(r.user_id);
    this.dispatch("someone_exit_room", room_member)
  }

  @INFO
  private _on_key_frames_recv(key_frames: KeyFrame[]): void {
    this.dispatch("key_frames", key_frames)
  }

  protected _video_input_device_id: string | null = null;
  protected _audio_input_device_id: string | null = null;
  protected _preview_video_input_track?: MediaStreamTrack;
  protected _preview_audio_input_track?: MediaStreamTrack;

  get video_input_device_id() { return this._video_input_device_id; };
  set_video_input_device_id(v: string | null) {
    const changed = this._video_input_device_id !== v;
    this._video_input_device_id = v;
    if (!changed) return Promise.resolve();
    return this._change_video_input(v);
  };

  get audio_input_device_id() { return this._audio_input_device_id; };
  set_audio_input_device_id(v: string | null) {
    const changed = this._audio_input_device_id !== v;
    this._audio_input_device_id = v;
    if (!changed) return Promise.resolve();
    return this._change_audio_input(v);
  };

  @INFO
  protected async _init_preview_stream(resolve: Resolve<MediaStream>, reject: Reject<any>) {
    if (!this.video_input_device_id || !this.audio_input_device_id)
      return resolve(new MediaStream());

    const constraints: MediaStreamConstraints = {
      video: this.video_input_device_id ? { deviceId: this.video_input_device_id } : false,
      audio: this.audio_input_device_id ? { deviceId: this.audio_input_device_id } : false
    };
    navigator.mediaDevices.getUserMedia(constraints)
      .then(stream => {
        const video_tracks = stream.getVideoTracks();
        const audio_tracks = stream.getAudioTracks();
        this._preview_video_input_track = video_tracks.length ? video_tracks[0] : void 0;
        this._preview_audio_input_track = audio_tracks.length ? audio_tracks[0] : void 0;
        resolve(stream);
      })
      .catch(reason => console.error(reason))
      .then(() => resolve(new MediaStream()));
  }

  @INFO
  clear_preview_stream() {
    const tracks = this.preview_stream.direct?.getTracks();
    if (tracks) {
      for (const track of tracks) {
        track.stop();
      }
    }
    this._preview_video_input_track = void 0;
    this._preview_audio_input_track = void 0;
    this.preview_stream.clear();
  }
  readonly preview_stream = new AnyHolder<MediaStream>(this._init_preview_stream.bind(this));

  @INFO
  protected async _change_audio_input(device_id: string | null) {
    return await this._change_any_input(device_id, 'audio');
  }

  @INFO
  protected async _change_video_input(device_id: string | null) {
    return await this._change_any_input(device_id, 'video');
  }
  @INFO_ARGS
  protected async _change_any_input(device_id: string | null, type: 'video' | 'audio') {
    const KEY_PREVIEW_TRACK = `_preview_${type}_input_track` as const;
    const KEY_GET_TRACKS = {
      video: 'getVideoTracks' as const,
      audio: 'getAudioTracks' as const
    }[type];

    const preview_stream = await this.preview_stream.value;
    if (!preview_stream)
      WARN.print('RTC._change_any_input', `preview_stream is ${preview_stream}`)


    const old_preview_track = this[KEY_PREVIEW_TRACK];
    if (old_preview_track) {
      old_preview_track.stop();
      preview_stream?.removeTrack(old_preview_track);
      this[KEY_PREVIEW_TRACK] = void 0;
      this._members.forEach(v => v.remove_track(old_preview_track))
    }

    if (typeof device_id === 'string') {
      const constraints: MediaStreamConstraints = { [type]: { deviceId: device_id } };
      const stream = await navigator.mediaDevices.getUserMedia(constraints)
      const tracks = stream[KEY_GET_TRACKS]();
      if (tracks.length)
        WARN.print('RTC._change_any_input', `length of tracks is ${tracks.length}`)

      const new_track = tracks.length ? tracks[0] : undefined;
      if (new_track) {
        stream.removeTrack(new_track);
        this[KEY_PREVIEW_TRACK] = new_track;
        preview_stream?.addTrack(new_track);
        this._members.forEach(v => v.add_track(new_track))
      }
    }

    if (!preview_stream?.getTracks().length) {
      this.clear_preview_stream();
    }
  }

  on(type: "key_frames", callback: (e: CustomEvent<KeyFrame[]>) => void, options?: boolean | EventListenerOptions | undefined): () => void;
  on<K extends keyof RTCEventMap>(type: K, listener: (ev: RTCEventMap[K]) => any, options?: boolean | AddEventListenerOptions): () => void;
  on(type: string, callback: any, options?: boolean | AddEventListenerOptions | undefined): () => void {
    this._et.addEventListener(type, callback, options);
    return () => this._et.removeEventListener(type, callback, options);
  }

  addEventListener(type: "key_frames", callback: (e: CustomEvent<KeyFrame[]>) => void, options?: boolean | EventListenerOptions | undefined): void;
  addEventListener<K extends keyof RTCEventMap>(type: K, callback: (e: RTCEventMap[K]) => void, options?: boolean | EventListenerOptions | undefined): void;
  addEventListener(type: string, callback: any, options?: boolean | AddEventListenerOptions | undefined): void {
    return this._et.addEventListener(type, callback, options)
  }


  dispatch(type: "key_frames", detail: KeyFrame[]): boolean
  dispatch<K extends keyof RTCEventMap>(type: K, detail: RTCEventMap[K]['detail']): boolean
  dispatch(type: string, detail: any): boolean {
    const ev = new CustomEvent(type, { detail })
    return this._et.dispatchEvent(ev);
  }


  removeEventListener(type: "key_frames", callback: (e: CustomEvent<KeyFrame[]>) => void, options?: boolean | EventListenerOptions | undefined): void;
  removeEventListener<K extends keyof RTCEventMap>(type: K, callback: (e: RTCEventMap[K]) => void, options?: boolean | EventListenerOptions | undefined): void;
  removeEventListener(type: string, callback: any, options?: boolean | EventListenerOptions | undefined): void {
    this._et.removeEventListener(type, callback, options)
  }

  release(): void {
    this._socket.direct?.disconnect();
  }
}